/*
 * Copyright (c) 2006 Charles S. Wilson
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a 
 * copy of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the 
 * Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included 
 * in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 
 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 
 * OTHER DEALINGS IN THE SOFTWARE.
 */

#if HAVE_CONFIG_H
# include <config.h>
#endif

#if STDC_HEADERS
# include <stdlib.h>
# include <stdarg.h>
# include <string.h>
# include <float.h>
#endif

#include <stdio.h>
#include <errno.h>

#if HAVE_SYS_TYPES_H
# include <sys/types.h>
#endif
#if HAVE_MALLOC_H
# include <malloc.h>
#endif
#if HAVE_SYS_STAT_H
# include <sys/stat.h>
#endif

#if HAVE_SYS_CYGWIN_H
# include <sys/cygwin.h>
#endif
#if HAVE_DLFCN_H
# include <dlfcn.h>
#endif
#if HAVE_X11_XLIB_H
# include <X11/Xlib.h>
#endif
#if HAVE_LOCALE_H
# include <locale.h>
#endif
#if HAVE_PTHREAD
# include <pthread.h>
#endif
#if HAVE_MATH_H
# include <math.h>
#endif

#if HAVE_WINDOWS_H && HAVE_OPENCLIPBOARD
# define WIN32_LEAD_AND_MEAN
# define NOMINMAX
# undef Status /* Xlib.h from xorg 7.5 is evil */
# include <windows.h>
#endif


#include "checkX.h"
#include "util.h"


#define XSERV_TIMEDOUT -1
#define XSERV_FOUND     0
#define XSERV_NOTFOUND  1

typedef Display*(*XOpenDisplayFP)(char*);
typedef int(*XCloseDisplayFP)(Display *);
typedef struct WorkerThreadData_ {
  char*           displayname;
  char*           pathToX11Lib;
} WorkerThreadData;

#define timerspec_add(a, b, result)                                           \
  do {                                                                        \
    (result)->tv_sec = (a)->tv_sec + (b)->tv_sec;                             \
    (result)->tv_nsec = (a)->tv_nsec + (b)->tv_nsec;                          \
    if ((result)->tv_nsec >= 1000000000)                                      \
      {                                                                       \
        ++(result)->tv_sec;                                                   \
        (result)->tv_nsec -= 1000000000;                                      \
      }                                                                       \
  } while (0)


static pthread_mutex_t mtx_xopenOK;
static pthread_cond_t  cv_xopenOK;
static int xopenOK;
static int xopenMayRetry;
static pthread_mutex_t mtx_threadOK;
static pthread_cond_t  cv_threadOK;

static const char* XLIBfmt = "cygX11-%d.dll";
static const char* DefaultAppendPath = "/usr/X11R6/bin" SEP_CHAR "/usr/bin";

/**********************************************************/
static int
adjust_path(const char*  prepend,
            const char*  append,
            const char*  replace,
                  char** newPath);

static int
find_X11_lib(const char*  searchPath,
             const char*  explicitName,
                   char** pathToX11Lib);

static int
dlopen_X11_lib(const char* pathToX11Lib,
               void**      handleToDll);

static int
load_X11_symbols(void*            handleToDll,
                 XOpenDisplayFP*  xopendisP,
                 XCloseDisplayFP* xclosedisP);

static int
test_Xserver(const char*     displayName,
             const char*     pathToX11Lib,
             double          timeout);

static void computeTimespec(double val, timespec_t* t);
static int try_with_timeout(WorkerThreadData* data, double delay);
static void* open_display(void* /* WorkerThreadData* */ v);
/**********************************************************/

static int
adjust_path(const char*  prepend,
            const char*  append,
            const char*  replace,
                  char** newPath)
{
  char* appendPath;
  char* origPath;
  int c;
  char* path;

  if (!append)
    appendPath = run2_strdup(DefaultAppendPath);
  else
    appendPath = run2_strdup(append);

  origPath = getenv("PATH");
  c = strlen(origPath);
  if (replace)     { c  = strlen(replace);         }
  if (appendPath)  { c += strlen(appendPath) + 1;  }
  if (prepend)     { c += strlen(prepend) + 1;     }

  path = (char*) run2_malloc ((c + 1) * sizeof(char*));
  path[0] = '\0';
  if (prepend) {
    strcat(path, prepend);
    strcat(path, SEP_CHAR);
  }
  if (replace) {
    strcat(path, replace);
  } else {
    strcat(path, origPath);
  }
  if (appendPath) {
    strcat(path, SEP_CHAR);
    strcat(path, appendPath);
  }
  path[c] = '\0'; /* paranoia */
  debugMsg(1, "(%s) path is : %s", __func__, path);

  *newPath = path;
  return 0;
}

static int
find_X11_lib(const char*  searchPath,
             const char*  explicitName,
                   char** pathToX11Lib)
{
  Ustr *bufFullPath;
  Ustr *tsearchPath;
  Ustr *texplicitName;
  Ustr *bufFileName;
  int i;

  tsearchPath = USTR_CHECK (ustr_dup_cstr (searchPath));

  if (explicitName)
    {
      texplicitName = USTR_CHECK (ustr_dup_cstr (explicitName));
      bufFullPath = run2_pfopen (texplicitName, tsearchPath);
      ustr_free (texplicitName);
    }
  else
    {
      bufFileName = USTR_CHECK (ustr_dup_empty ());
      for (i = 9; i > 5; i--)
        {
          ustr_set_fmt (&bufFileName, XLIBfmt, i);
          bufFullPath = run2_pfopen (bufFileName, tsearchPath);
          if (bufFullPath)
            {
              if (run2_fileExists (NULL, NULL, bufFullPath))
                break;
              else
                ustr_free (bufFullPath);
            }
        }
      ustr_free (bufFileName);
    }
  ustr_free (tsearchPath);

  if (!bufFullPath || ustr_len (bufFullPath) == 0)
    {
      errorMsg("could not locate Xlib DLL %s", (explicitName ? explicitName : XLIBfmt));
      if (bufFullPath) ustr_free (bufFullPath);
      return 1;
    }
  *pathToX11Lib = run2_strdup (ustr_cstr (bufFullPath));
  ustr_free (bufFullPath);
  debugMsg(1, "(%s) DLL is %s", __func__, *pathToX11Lib);

  return 0;
}

static int
dlopen_X11_lib(const char* pathToX11Lib,
               void**      handleToDll)
{
  void* handle = NULL;

  handle = dlopen(pathToX11Lib, RTLD_LAZY | RTLD_GLOBAL);
  if (!handle) {
    errorMsg("problem loading %s: %s", pathToX11Lib, dlerror());
    return 1;
  }
  debugMsg(1, "(%s) %s dlopen'ed successfully.", __func__, pathToX11Lib);
  *handleToDll = handle;
  return 0;
}

static int
load_X11_symbols(void*            handleToDll,
                 XOpenDisplayFP*  xopendisP,
                 XCloseDisplayFP* xclosedisP)
{
  XOpenDisplayFP xopendis = (XOpenDisplayFP)NULL;
  XCloseDisplayFP xclosedis = (XCloseDisplayFP)NULL;

  xopendis =  (XOpenDisplayFP) dlsym(handleToDll, "XOpenDisplay");
  if (!xopendis)
  {
    errorMsg("problem loading symbol XOpenDisplay: %s", dlerror());
    return 1;
  }
  debugMsg(1, "(%s) symbol XOpenDisplay loaded ok", __func__);

  xclosedis = (XCloseDisplayFP)dlsym(handleToDll, "XCloseDisplay");
  if (!xclosedis)
  {
    errorMsg("problem loading symbol XCloseDisplay: %s", dlerror());
    return 1;
  }
  debugMsg(1, "(%s) symbol XCloseDisplay loaded ok", __func__);

  *xopendisP = xopendis;
  *xclosedisP = xclosedis;
  return 0;
}

static int
test_Xserver(const char*     displayName,
             const char*     pathToX11Lib,
             double          timeout)
{
  WorkerThreadData data;
  int rc;
  char* dispName;
  char* x11Path;

  /* paranoia */
  if (!displayName || !*displayName)
    {
      errorMsg ("Internal error: %s called with NULL displayName", __func__);
      return 1;
    }
  if (!pathToX11Lib || !*pathToX11Lib)
    {
      errorMsg ("Internal error: %s called with NULL pathToX11Lib", __func__);
      return 1;
    }
  dispName = (displayName ? run2_strdup(displayName) : NULL);
  x11Path  = (pathToX11Lib ? run2_strdup(pathToX11Lib) : NULL);

  data.pathToX11Lib = x11Path;
  data.displayname = dispName;

  rc = try_with_timeout(&data, timeout);

  if (rc == 0) {
    infoMsg("X display '%s' successfully opened",
             (displayName ? displayName : ""));
  } else {
    infoMsg("could not open X display '%s'",
            (displayName ? displayName : ""));
  }

  if (dispName)
    {
      data.displayname = NULL;
      free (dispName);
      dispName = NULL;
    }
  if (x11Path)
    {
      data.pathToX11Lib = NULL;
      free (x11Path);
      x11Path = NULL;
    }
  return rc;
}

static void
computeTimespec(double val, timespec_t* t)
{
   double frac;
   if (!t) return;
   if (val < 0) return;
   t->tv_sec = (time_t)(floor(val));
   frac = (val - (double)t->tv_sec);
   t->tv_nsec = (long)(1000000000.0 * frac);
}

static int
try_with_timeout(WorkerThreadData* data, double delay)
{
  pthread_attr_t attr;
  pthread_t      id;
  int            status;
  timespec_t     now;
  timespec_t     delta;
  timespec_t     then;
  int            rValue;
  int            rc;

  xopenMayRetry = delay!=0.0; /* false actually means: try once */
  xopenOK = XSERV_NOTFOUND; /* a pessimistic start out */
  rValue = XSERV_NOTFOUND;

  computeTimespec (fabs(delay), &delta);
  debugMsg(1, "(%s) Using delay of %d secs, %ld nanosecs (%5.2f)", __func__,
           delta.tv_sec, delta.tv_nsec,
           (double)delta.tv_sec + ((double)delta.tv_nsec)/1000000000.0);

  pthread_mutex_init (&mtx_xopenOK, NULL);
  pthread_cond_init (&cv_xopenOK, NULL);

  pthread_mutex_init (&mtx_threadOK, NULL);
  pthread_cond_init (&cv_threadOK, NULL);

  pthread_attr_init (&attr);
  pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_JOINABLE);

  pthread_mutex_lock (&mtx_xopenOK);
  pthread_mutex_lock (&mtx_threadOK);

  /* create thread, and wait for it to signal startup init complete */
  pthread_create (&id, &attr, open_display, (void*)data);
  pthread_cond_wait (&cv_threadOK, &mtx_threadOK);
  pthread_mutex_unlock (&mtx_threadOK);

  if (delay != 0.0) {
    clock_gettime (CLOCK_REALTIME, &now);
    timerspec_add (&now, &delta, &then);
    rc = pthread_cond_timedwait (&cv_xopenOK, &mtx_xopenOK, &then);
    if (rc == ETIMEDOUT)
      {
        /* timedout. We have the xopenOK mutex */
        xopenOK = XSERV_TIMEDOUT;
        xopenMayRetry = 0;
        rValue = XSERV_NOTFOUND;
      }
    else if (rc != 0)
      {
        /* something else caused us to wake up early and unsuccessful */
        xopenOK = XSERV_NOTFOUND;
        rValue = XSERV_NOTFOUND;
      }
    else
      {
        /* open_display() was successful. We have the xopenOK mutex */
        rValue = xopenOK;
      }
    pthread_detach (id);  /* leave open_display() on its own */
  } else {
    pthread_mutex_unlock (&mtx_xopenOK); /* allow open_display() to set xopenOK */
    pthread_join (id, (void*)&status); /* and wait for it */
    rValue = (xopenOK == XSERV_TIMEDOUT ? XSERV_NOTFOUND : xopenOK );
  }

  /* Clean up mutexes and condition variables                         */
  /* Unfortunately, these are all race conditions, so we simply punt. */
  /* Once the app exits, these will be cleaned up automatically.      */
  /*
  pthread_mutex_destroy (&mtx_threadOK);
  pthread_cond_destroy (&cv_threadOK);
  pthread_mutex_destroy (&mtx_xopenOK);
  pthread_cond_destroy (&cv_xopenOK);
  */
  pthread_attr_destroy (&attr);

  debugMsg(1, "(%s) xserver search was %s", __func__,
           (xopenOK == XSERV_TIMEDOUT ? "timed out" :
           (xopenOK == XSERV_NOTFOUND ? "unsuccessful" : "a success!")));

  return (rValue);
}

/* replacement function emulates pthread_mutex_timedlock, which
 * is not available in cygwin's pthread implementation. Accurate
 * only to the resolution of gettimeofday.  run2_usleep is used
 * to guarantee the specified sleep time, even if the sleep is
 * interrupted by a signal.
 */
static int
run2_usleep(unsigned long usec)
{
    struct timespec req = {0};
    time_t sec = (int)(usec/1000000);
    usec = usec - (sec * 1000000);
    req.tv_sec = sec;
    req.tv_nsec = usec * 1000L;
    while (nanosleep (&req, &req) == -1)
      continue;
    return 1;
}

static void*
open_display (void* /* WorkerThreadData* */ v)
{
  Display* dpy;
  WorkerThreadData* in_data;
  WorkerThreadData data;
  int keep_trying = 1;
  int rc;
  int dieNow = 0;
  void* handleToDll = NULL;
  XOpenDisplayFP xopendis = NULL;
  XCloseDisplayFP xclosedis = NULL;

  /* begin thread initialization */
  pthread_mutex_lock (&mtx_threadOK);
  in_data = (WorkerThreadData*)v;

  /* make a local copy of data */
  data.pathToX11Lib = run2_strdup (in_data->pathToX11Lib);
  data.displayname = run2_strdup (in_data->displayname);

  /* XLIB library load */
  rc = dlopen_X11_lib(data.pathToX11Lib, &handleToDll);
  if (rc != 0)
  {
    dieNow = 1;
  }

  /* XLIB symbol load */
  rc = load_X11_symbols(handleToDll, &xopendis, &xclosedis);
  if (rc != 0)
  {
    dieNow = 1;
  }

  /* let main thread know that we have completed initialization */
  pthread_cond_signal (&cv_threadOK);
  pthread_mutex_unlock (&mtx_threadOK);

  if (dieNow) goto cleanup;

  do
    {
      if((dpy = (*(xopendis))(data.displayname)))
        {
          /* successfully opened display */
          (*(xclosedis))(dpy);
          pthread_mutex_lock (&mtx_xopenOK);
          xopenOK = XSERV_FOUND;
          pthread_cond_signal (&cv_xopenOK);
          pthread_mutex_unlock (&mtx_xopenOK);
        }
      else
        {
          /* if we fail to open the display -- but return from XOpenDisplay
           * quickly and the main thread has a long timeout, then we will
           * try again (and again, and again) until the main thread stops us.
           * However, we don't want to hammer the Xserver if it is in the
           * process of starting up. So, we wait a certain amount before
           * retrying. This usually will only happen when starting the
           * Xserver, and using checkX as a 'barrier':
           *
           *    XWin --various --options
           *    # wait for server to launch... 
           *    checkX --display $DISPLAY --timeout 12
           *    # start other clients:
           *    xterm --some --options
           *
           * It would be nice to make this relatively fast (e.g. not
           * not milliseconds, but maybe 0.25 secs?) but the cygwin XWin
           * xserver has a problem in that it will prematurely allow some
           * successful XOpenDisplay calls, but later ones may fail. This
           * breaks the 'barrier' mode of operation.  So, knowing this, we
           * use a relatively long timeout here, like 5 seconds. This will
           * at least allow us to try three times, if the overall timeout
           * is 12 seconds.
           */
          keep_trying = (xopenMayRetry && xopenOK == XSERV_NOTFOUND);
          if (keep_trying)
            run2_usleep (5000000); /* 5 seconds */
        }
      /* recompute keep_trying; it may have changed while sleeping in else-clause */
      keep_trying = (xopenMayRetry && xopenOK == XSERV_NOTFOUND);
    }
  while (keep_trying);

cleanup:
  /* clean up local copy of data */
  free (data.displayname);
  data.displayname = NULL;
  free (data.pathToX11Lib);
  data.pathToX11Lib = NULL;

  dlclose(handleToDll);
  pthread_exit((void*)0);
  /* not reached */
  return (void*)0;
}

int
run2_checkX(const char* prepend,       /* can be NULL */
            const char* append,        /* can be NULL - uses default value */
            const char* replace,       /* can be NULL */
            const char* explicitName,  /* can be NULL - uses search pattern */
            const char* displayName,   /* can be NULL - XOpenDisplay defaults to :0 */
            double      timeout,       /* 0 means XOpenDisplay's builtin timeout of 12 s */
            char**      pathToX11Lib)  /* if not NULL, caller must free */
{
  int rc;
  char* pathList = NULL;
  char* x11LibPath = NULL;
  const char* dispVar;

  /***********************
   * PATH handling
   ***********************/
  rc = adjust_path(prepend,
                   append,
                   replace,
                   &pathList);
  if (rc != 0)
  {
    return 1;
  }

  /***********************
   * XLIB library name
   ***********************/
  rc = find_X11_lib(pathList,
                    explicitName,
                    &x11LibPath);
  free(pathList);
  pathList = NULL;
  if (rc != 0)
  {
    return 1;
  }

  /***********************
   * Test XServer
   ***********************/
  dispVar = displayName ? displayName : getenv("DISPLAY");
  if (!dispVar || !*dispVar)
    {
      infoMsg ("Unspecified X display (check --display in xml <SelfOptions>"
               " or in cmdline; also check $DISPLAY environment variable).");
      free(x11LibPath);
      return 1;
    }
  rc = test_Xserver(dispVar,
                    x11LibPath,
                    timeout);
  if (rc != 0)
  {
    free(x11LibPath);
    return 1;
  }

  if (pathToX11Lib)
  {
    *pathToX11Lib = x11LibPath;
  }
  else
  {
    free(x11LibPath);
  }
  return 0;
}

